/*
 *  Copyright 2007 Blandware (http://www.blandware.com)
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.blandware.atleap.webapp.taglib.core.content;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.validator.GenericValidator;
import org.apache.struts.taglib.TagUtils;

import javax.servlet.jsp.tagext.SimpleTagSupport;
import javax.servlet.jsp.tagext.JspFragment;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.PageContext;
import javax.servlet.jsp.JspTagException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.ServletException;
import java.io.IOException;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.util.Arrays;
import java.util.Map;

import com.blandware.atleap.webapp.util.core.ApplicationResources;
import com.blandware.atleap.webapp.util.core.WebappUtil;
import com.blandware.atleap.webapp.util.core.WebappConstants;
import com.blandware.atleap.webapp.util.core.RequestUtil;
import com.blandware.atleap.webapp.struts.ContentTilesRequestProcessor;
import com.blandware.atleap.webapp.taglib.core.util.ContextMenuItem;
import com.blandware.atleap.webapp.taglib.core.util.JavaScriptUtil;

/**
 * <p>While in edit mode, this tag allows to wrap some content in a DIV with
 * context menu which gives ability to perform 3 actions with the content:
 * 'edit element', 'create element' or 'delete element'. It is assumed, that
 * content represents some element of some collection, so 'creation' or
 * 'deletion' means creation of new element in this collection, or deletion of
 * element from this collection.</p>
 * <p>List of allowed attributes:
 * <ul>
 * <li>
 * <b>editAction</b> - context-relative URI of Struts Action to call to update
 * element which is placed inside this tag
 * </li>
 * <li>
 * <b>editForward</b> - name of global Action Forward to use to update element
 * which is placed inside this tag
 * </li>
 * <li>
 * <b>editUri</b> - context-relative URI of action to call to update element
 * which is placed inside this tag
 * </li>
 * <li>
 * <b>createAction</b> - context-relative URI of Struts Action to call to create
 * a new element in collection
 * </li>
 * <li>
 * <b>createForward</b> - name of global Action Forward to use to create a new
 * element in collection
 * </li>
 * <li>
 * <b>createUri</b> - context-relative URI of action to call to create a new
 * element in collection
 * </li>
 * <li>
 * <b>deleteAction</b> - context-relative URI of Struts Action to call to delete
 * element which is placed inside this tag
 * </li>
 * <li>
 * <b>deleteForward</b> - name of global Action Forward to use to delete element
 * which is placed inside this tag
 * </li>
 * <li>
 * <b>deleteUri</b> - context-relative URI of action to call to delete element
 * which is placed inside this tag
 * </li>
 * <li>
 * <b>localizedEdit</b> - If <code>true</code>, there will be three
 * 'edit actions': in current locale, in all locales and in selected locales.
 * A special parameter 'localesMode' will be appended to URI for edit.
 * </li>
 * <li>
 * <b>contextMenuJsp</b> - path to JSP which will render a context menu. If not
 * given, <code>WebappConstants.DEFAULT_CONTEXT_MENU_JSP</code> is used.
 * </li>
 * </ul></p>
 * <p>
 * At least one of <b>*Action</b>, <b>*Forward</b> or <b>*Uri</b> must be
 * specified. If, for instance, nothing is specified for 'create*', there will
 * not be 'Create' item in context menu.</p>
 * <p>When calculating URI, firstly
 * <b>*Action</b> is considered, then (if it's <code>null</code>),
 * <b>*Forward</b> is taken into account. If it's <code>null</code> too,
 * <b>*Uri</b> is used.
 * </p>
 * <p>Here's an example:
 * <pre>
 * &lt;atleap:constants className="com.blandware.atleap.common.Constants"
 *     var="RESOURCE_TYPE_IMAGE" scope="page" /&gt;
 * &lt;atleap:resourceIterator var="image"
 *     uriPrefix="/rw/resource/photos/" orderBy="r.uri"&gt;
 *     &lt;atleap:rewriteUrl href="${image.uri}" var="src" /&gt;
 *     &lt;atleap:fieldValue valueMap="${image.description}"
 *         var="description" filter="true" /&gt;
 *     &lt;atleap:rewriteUrl var="editUrl"
 *         action="core/contentResource/callUpdate"&gt;
 *         &lt;atleap:param name="id" value="${image.id}"/&gt;
 *     &lt;/atleap:rewriteUrl&gt;
 *     &lt;atleap:rewriteUrl var="createUrl"
 *         action="core/contentResource/callCreate"&gt;
 *         &lt;atleap:param name="folder" value="/photos/${index}/" /&gt;
 *         &lt;atleap:param name="type"
 *             value="${pageScope.RESOURCE_TYPE_IMAGE}" /&gt;
 *     &lt;/atleap:rewriteUrl&gt;
 *     &lt;atleap:rewriteUrl var="deleteUrl"
 *         action="core/contentResource/delete"&gt;
 *         &lt;atleap:param name="id" value="${image.id}"/&gt;
 *     &lt;/atleap:rewriteUrl&gt;
 *     &lt;atleap:editCollection editUri="${editUrl}"
 *         createUri="${createUrl}" deleteUri="${deleteUrl}"&gt;
 *         &lt;img alt="${description}" src="${src}"&gt;
 *     &lt;/atleap:editCollection&gt;
 * &lt;/atleap:resourceIterator&gt;
 * </pre>
 * This will generate a sequence of images taken from /photos folder and ordered
 * by their URI. Each image will be equipped with descriptive alt. In edit mode,
 * it will be possible to create, edit and delete images through context menu.
 * Note that action for image creation is supplied with 'type' parameter, so it
 * will be impossible to create content resource of another type (like document
 * or file).</p>
 * <p><a href="EditCollectionTag.java.html"><i>View source</i></a></p>
 *
 * @author Roman Puchkovskiy <a href="mailto:roman.puchkovskiy@blandware.com">
 *         &lt;roman.puchkovskiy@blandware.com&gt;</a>
 * @author Sergey Zubtsovskiy <a href="mailto:sergey.zubtsovskiy@blandware.com">&lt;sergey.zubtsovskiy@blandware.com&gt;</a>
 * @version $Revision: 1.5 $ $Date: 2007/04/28 11:21:32 $
 * @see com.blandware.atleap.webapp.taglib.core.content.EditTag
 * @jsp.tag name="editCollection"
 * body-content="scriptless"
 */
public class EditCollectionTag extends SimpleTagSupport {
    protected transient final Log log = LogFactory.getLog(EditCollectionTag.class);
    protected static final String NUMBER_KEY = "com.blandware.atleap.taglib.content.EDIT_COLLECTION_TAG_NUMBER";

    /**
     * Context-relative URI of Struts Action to call to update collection
     * element content which is placed inside this tag
     */
    protected String editAction = null;

    /**
     * Name of global ActionForward to use to update collection
     * element content which is placed inside this tag
     */
    protected String editForward = null;

    /**
     * Context-relative URI of action to call to update collection
     * element content which is placed inside this tag
     */
    protected String editUri = null;

    /**
     * Context-relative URI of Struts Action to call to create a new collection
     * element
     */
    protected String createAction = null;

    /**
     * Name of global ActionForward to use to create a new collection
     * element
     */
    protected String createForward = null;

    /**
     * Context-relative URI of action to call to update create a new collection
     * element
     */
    protected String createUri = null;

    /**
     * Context-relative URI of Struts Action to call to delete collection
     * element which is placed inside this tag
     */
    protected String deleteAction = null;

    /**
     * Name of global ActionForward to use to delete collection
     * element which is placed inside this tag
     */
    protected String deleteForward = null;

    /**
     * Context-relative URI of action to call to delete collection
     * element which is placed inside this tag
     */
    protected String deleteUri = null;

    /**
     * If true, there will be three 'edit actions': in current locale, in all
     * locales and in selected locales. A special parameter 'localesMode' will
     * be appended to URI for edit.
     */
    protected Boolean localizedEdit = Boolean.FALSE;

    /**
	 * Path to JSP which will render a context menu
	 */
	protected String contextMenuJsp = null;

    /**
     * Returns Struts Action for editing.
     *
     * @return Struts Action for editing
     * @see #editAction
     * @jsp.attribute required="false"
     * rtexprvalue="true"
     * type="java.lang.String"
     * description="Context-relative URI of Struts Action to call to update collection element which is placed inside this tag"
     */
    public String getEditAction() {
        return editAction;
    }

    /**
     * Sets Struts Action for editing.
     *
     * @param action Struts Action to set
     * @see #editAction
     */
    public void setEditAction(String action) {
        this.editAction = action;
    }

    /**
     * Returns ActionForward for editing.
     *
     * @return ActionForward for editing
     * @see #editForward
     * @jsp.attribute required="false"
     * rtexprvalue="true"
     * type="java.lang.String"
     * description="Name of global ActionForward to use to update collection element which is placed inside this tag"
     */
    public String getEditForward() {
        return editForward;
    }

    /**
     * Sets ActionForward for editing.
     *
     * @param forward ActionForward to set
     * @see #editForward
     */
    public void setEditForward(String forward) {
        this.editForward = forward;
    }

    /**
     * Returns URI for editing.
     *
     * @return URI for editing
     * @see #editUri
     * @jsp.attribute required="false"
     * rtexprvalue="true"
     * type="java.lang.String"
     * description="Context-relative URI to call to update collection element which is placed inside this tag"
     */
    public String getEditUri() {
        return editUri;
    }

    /**
     * Sets URI for editing.
     *
     * @param uri URI to set
     * @see #editUri
     */
    public void setEditUri(String uri) {
        this.editUri = uri;
    }

    /**
     * Returns Struts Action for creating.
     *
     * @return Struts Action for creating
     * @see #createAction
     * @jsp.attribute required="false"
     * rtexprvalue="true"
     * type="java.lang.String"
     * description="Context-relative URI of Struts Action to call to create a new collection element"
     */
    public String getCreateAction() {
        return createAction;
    }

    /**
     * Sets Struts Action for creating.
     *
     * @param action Struts Action to set
     * @see #createAction
     */
    public void setCreateAction(String action) {
        this.createAction = action;
    }

    /**
     * Returns ActionForward for creating.
     *
     * @return ActionForward for creating
     * @see #createForward
     * @jsp.attribute required="false"
     * rtexprvalue="true"
     * type="java.lang.String"
     * description="Name of global ActionForward to use to create a new collection element"
     */
    public String getCreateForward() {
        return createForward;
    }

    /**
     * Sets ActionForward for creating.
     *
     * @param forward ActionForward to set
     * @see #createForward
     */
    public void setCreateForward(String forward) {
        this.createForward = forward;
    }

    /**
     * Returns URI for creating.
     *
     * @return URI for creating
     * @see #createUri
     * @jsp.attribute required="false"
     * rtexprvalue="true"
     * type="java.lang.String"
     * description="Context-relative URI to call to create a new collection element"
     */
    public String getCreateUri() {
        return createUri;
    }

    /**
     * Sets URI for creating.
     *
     * @param uri URI to set
     * @see #createUri
     */
    public void setCreateUri(String uri) {
        this.createUri = uri;
    }

    /**
     * Returns address of JSP that will be used to render a context menu
     *
     * @return address of JSP for context menu
     * @jsp.attribute required="false"
     * rtexprvalue="true"
     * type="java.lang.String"
     * description="Path to JSP which will render a context menu"
     */
    public String getContextMenuJsp() {
        return contextMenuJsp;
    }

    /**
     * Returns Struts Action for deleting.
     *
     * @return Struts Action for deleting
     * @see #deleteAction
     * @jsp.attribute required="false"
     * rtexprvalue="true"
     * type="java.lang.String"
     * description="Context-relative URI of Struts Action to call to delete collection element which is placed inside this tag"
     */
    public String getDeleteAction() {
        return deleteAction;
    }

    /**
     * Sets Struts Action for deleting.
     *
     * @param action Struts Action to set
     * @see #deleteAction
     */
    public void setDeleteAction(String action) {
        this.deleteAction = action;
    }

    /**
     * Returns ActionForward for deleting.
     *
     * @return ActionForward for deleting
     * @see #deleteForward
     * @jsp.attribute required="false"
     * rtexprvalue="true"
     * type="java.lang.String"
     * description="Name of global ActionForward to use to delete collection element which is placed inside this tag"
     */
    public String getDeleteForward() {
        return deleteForward;
    }

    /**
     * Sets ActionForward for deleting.
     *
     * @param forward ActionForward to set
     * @see #deleteForward
     */
    public void setDeleteForward(String forward) {
        this.deleteForward = forward;
    }

    /**
     * Returns URI for deleting.
     *
     * @return URI for deleting
     * @see #deleteUri
     * @jsp.attribute required="false"
     * rtexprvalue="true"
     * type="java.lang.String"
     * description="Context-relative URI to call to delete collection element which is placed inside this tag"
     */
    public String getDeleteUri() {
        return deleteUri;
    }

    /**
     * Sets URI for deleting.
     *
     * @param uri URI to set
     * @see #deleteUri
     */
    public void setDeleteUri(String uri) {
        this.deleteUri = uri;
    }

    /**
     * Returns whether 'edit action' is represented with three options: in
     * current locale, in all locales, in selected locales.
     *
     * @return whether edit is localized
     * @see #localizedEdit
     * @jsp.attribute required="false"
     * rtexprvalue="true"
     * type="java.lang.Boolean"
     * description="Whether 'edit action' is represented with three options: in current locale, in all locales, in selected locales"
     */
    public Boolean getLocalizedEdit() {
        return localizedEdit;
    }

    /**
     * Sets whether 'edit action' is represented with three options: in
     * current locale, in all locales, in selected locales.
     *
     * @param localizedEdit value to set
     * @see #localizedEdit
     */
    public void setLocalizedEdit(Boolean localizedEdit) {
        this.localizedEdit = localizedEdit;
    }

    /**
     * Sets address of JSP that will be used to render a context menu
     *
     * @param contextMenuJsp address of JSP for context menu to set
     */
    public void setContextMenuJsp(String contextMenuJsp) {
        this.contextMenuJsp = contextMenuJsp;
    }

    /**
     * Processes the tag.
     *
     * @throws javax.servlet.jsp.JspException
     * @throws java.io.IOException
     */
	public void doTag() throws JspException, IOException {

		PageContext pageContext = (PageContext) getJspContext();
		HttpServletRequest request = (HttpServletRequest) pageContext.getRequest();
		ApplicationResources applicationResources = ApplicationResources.getInstance(pageContext.getServletContext());

		if ( editUri == null && editAction == null && editForward == null
          && createUri == null && createAction == null && createForward == null
          && deleteUri == null && deleteAction == null && deleteForward == null) {
			JspTagException e = new JspTagException("One of the attributes 'editAction', 'editForward', 'editUri', 'createAction', 'createForward', 'createUri', 'deleteAction', 'deleteForward', 'deleteUri' must be specified");
			throw e;
		}

        editUri = calculateUri(editAction, editForward, editUri, request);
        createUri = calculateUri(createAction, createForward, createUri, request);
        deleteUri = calculateUri(deleteAction, deleteForward, deleteUri, request);

        boolean editExists = editUri != null;
        boolean createExists = createUri != null;
        boolean deleteExists = deleteUri != null;

        if (localizedEdit == null) {
            localizedEdit = Boolean.FALSE;
        }

        // calculate tag number
		Integer tagNumber = (Integer) pageContext.getAttribute(NUMBER_KEY, PageContext.REQUEST_SCOPE);
		if ( tagNumber == null ) {
			tagNumber = new Integer(0);
		} else {
			tagNumber = new Integer(tagNumber.intValue() + 1);
		}

		pageContext.setAttribute(NUMBER_KEY, tagNumber, PageContext.REQUEST_SCOPE);

		String content = null;
		boolean contentExists = true;
		StringWriter sw = new StringWriter();
		JspFragment body = getJspBody();
		if ( body != null ) {
			getJspBody().invoke(sw);
			content = sw.toString();
			contentExists = content.trim().length() > 0;
		} else {
			contentExists = false;
		}

		TagUtils tagUtils = TagUtils.getInstance();
		if ( !contentExists ) {
			content = applicationResources.getMessage(request, "core.commons.contentTag.insertText");
		}

		if ( GenericValidator.isBlankOrNull(contextMenuJsp) ) {
			contextMenuJsp = WebappConstants.DEFAULT_CONTEXT_MENU_JSP;
		}

		Object editModeEnabled = request.getSession().getAttribute(WebappConstants.SITE_EDIT_MODE_ENABLED_KEY);
		boolean createWrapper = editModeEnabled != null && Boolean.TRUE.equals(editModeEnabled);
		if ( createWrapper ) {
			StringBuffer divId = new StringBuffer("__editable__element__").append(tagNumber).append("__wrapper__");
			String requestUrl = (String) request.getAttribute(
                    ContentTilesRequestProcessor.PROCESSED_URL);

			// make context menu ID
			StringBuffer contextMenuId = new StringBuffer("__contextMenu__element__").append(tagNumber.toString());

            // make URIs for context menu
            String uriForCurrentLocale = null;
            String uriForAllLocales = null;
            String uriForChosenLocales = null;
            String defaultEditUri = editUri;
            if (editExists && localizedEdit.booleanValue()) {
                Map params = RequestUtil.getRequestParametersFromUri(editUri);
                String base = RequestUtil.getBaseFromUri(editUri);
                params.put("localesMode", "current");
                uriForCurrentLocale = RequestUtil.appendParams(base, params);
                params.put("localesMode", "all");
                uriForAllLocales = RequestUtil.appendParams(base, params);
                uriForChosenLocales = editUri;
                defaultEditUri = uriForCurrentLocale;
            }

            /*
                  That's a name of JS variable that will hold this
                  context menu (that variable is used to refer to this context
                  menu from showContextMenu(), for instance)
               */
			StringBuffer contextMenuLayerVar = new StringBuffer(contextMenuId.toString());

			StringBuffer onmouseover = new StringBuffer("doSelectLayer(this.id);");
			StringBuffer onmouseout = new StringBuffer("doUnselectLayer(this.id);");
			StringBuffer ondblclick = new StringBuffer("doCallContentUpdate('").append(defaultEditUri).append("', '").append(requestUrl).append("');");
			StringBuffer oncontextmenu = new StringBuffer("return showContextMenu(").append(contextMenuLayerVar).append(", event);");

			String divDisplay = "block";

			// append content to wrapper and close DIV
			StringBuffer wrappedContent = new StringBuffer("<div id=\"").append(divId)
			        .append("\" name=\"").append(divId)
			        .append("\" class=\"fieldValueWrapper\" style=\"display: ")
			        .append(divDisplay).append(";\" ")
                    .append("onmouseover=\"").append(onmouseover)
                    .append("\" onmouseout=\"").append(onmouseout);
            if (editExists) {
                wrappedContent.append("\" ondblclick=\"").append(ondblclick);
            }
            wrappedContent.append("\" oncontextmenu=\"").append(oncontextmenu)
                    .append("\">").append(content).append("</div>");

			tagUtils.write(pageContext, wrappedContent.toString());

			// Insert context menu
			// Obtain titles
            String editTitle = null;
            String createTitle = null;
            String deleteTitle = null;
            String editInCurrentLocaleTitle = null;
            String editInAllLocalesTitle = null;
            String editInChosenLocalesTitle = null;

            if (editExists) {
                if (localizedEdit.booleanValue()) {
                    editInCurrentLocaleTitle = applicationResources.getMessage(request,
                            "core.editMode.menu.context.editInCurrentLocale");
                    editInAllLocalesTitle = applicationResources.getMessage(request,
                            "core.editMode.menu.context.editInAllLocales");
                    editInChosenLocalesTitle = applicationResources.getMessage(request,
                            "core.editMode.menu.context.editInChosenLocales");
                    editTitle = editInCurrentLocaleTitle;
                } else {
                    editTitle = applicationResources.getMessage(request,
                            "core.editMode.menu.context.edit");
                }
            }
            if (createExists) {
                createTitle = applicationResources.getMessage(request,
                        "core.editMode.menu.context.create");
            }
            if (deleteExists) {
                deleteTitle = applicationResources.getMessage(request,
                        "core.editMode.menu.context.delete");
            }

            String normalClass = WebappConstants.CONTEXT_MENU_ITEM_STYLE_CLASS;
            String hltdClass
                    = WebappConstants.HIGHLIGHTED_CONTEXT_MENU_ITEM_STYLE_CLASS;

            // Create and populate list of items
			ContextMenuItem menu = new ContextMenuItem();
			menu.setId(contextMenuId.toString());
            if (editExists) {
                if (localizedEdit.booleanValue()) {
                    menu.addChildItem(new ContextMenuItem(JavaScriptUtil.createLinkAsLayer("javascript:doCallContentUpdate('" + uriForCurrentLocale + "', '" + requestUrl + "');",
                            editInCurrentLocaleTitle, normalClass, hltdClass)));
                    menu.addChildItem(new ContextMenuItem(JavaScriptUtil.createLinkAsLayer("javascript:doCallContentUpdate('" + uriForAllLocales + "', '" + requestUrl + "');",
                            editInAllLocalesTitle, normalClass, hltdClass)));
                    menu.addChildItem(new ContextMenuItem(JavaScriptUtil.createLinkAsLayer("javascript:doCallContentUpdate('" + uriForChosenLocales + "', '" + requestUrl + "');",
                            editInChosenLocalesTitle, normalClass, hltdClass)));
                } else {
                    menu.addChildItem(new ContextMenuItem(JavaScriptUtil.createLinkAsLayer("javascript:doCallContentUpdate('" + defaultEditUri + "', '" + requestUrl + "');",
                            editTitle, normalClass, hltdClass)));
                }
            }
            if (createExists) {
                menu.addChildItem(new ContextMenuItem(JavaScriptUtil.createLinkAsLayer("javascript:doCallCreateElement('" + createUri + "', '" + requestUrl + "');",
                        createTitle, normalClass, hltdClass)));
            }
            if (deleteExists) {
                menu.addChildItem(new ContextMenuItem(JavaScriptUtil.createLinkAsLayer("javascript:doCallDeleteElement('" + deleteUri + "', '" + requestUrl + "');",
                        deleteTitle, normalClass, hltdClass)));
            }

            // Store menu in request
			pageContext.setAttribute(WebappConstants.CONTEXT_MENUS_KEY,
                                     Arrays.asList(new ContextMenuItem[]{menu}),
                                     PageContext.REQUEST_SCOPE);

			// Do include
			try {
				pageContext.include(contextMenuJsp);
			} catch ( ServletException ex ) {
				JspTagException e = new JspTagException(ex);
				TagUtils.getInstance().saveException(pageContext, e);
				throw e;
			}
		} else {
			tagUtils.write(pageContext, content);
		}
	}

    /**
     * Calculates URI based from action, forward, URI.
     *
     * @param action action to use
     * @param forward forward to use
     * @param uri URI to use
     * @param request request currently processed
     * @return calculated URI
     * @throws JspTagException thrown if something goes wrong
     */
    protected String calculateUri(String action, String forward,
                                String uri, HttpServletRequest request)
            throws JspTagException {
        String result = uri;
        if ( action != null ) {
			result = WebappUtil.getActionMappingURL(action, null, request, WebappConstants.URL_TYPE_DOMAIN_RELATIVE);
		} else if ( forward != null ) {
			try {
				result = WebappUtil.getActionForwardURL(forward, null, request, WebappConstants.URL_TYPE_DOMAIN_RELATIVE);
			} catch ( MalformedURLException e ) {
				JspTagException ex = new JspTagException(e);
				throw ex;
			}
		}
        return result;
    }
}
